﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.ServiceModel.DomainServices.Client;
using System.Threading;
using Xceed.Silverlight.Data.Stats;

namespace Xceed.Silverlight.Data
{
  public abstract class WcfRiaServicesDataSourceProviderBase<TEntity> : AsyncDataSourceProvider, ICountRestrictedProvider where TEntity : Entity
  {
    #region Constructors

    static WcfRiaServicesDataSourceProviderBase()
    {
      SharedMetadata.DisplayAttributeAssemblyQualifiedName = typeof( DisplayAttribute ).AssemblyQualifiedName;
    }

    protected WcfRiaServicesDataSourceProviderBase(
      EntityQuery<TEntity> entityQuery,
      DomainContext domainContext )
    {
      if( entityQuery == null )
        throw new ArgumentNullException( "entityQuery" );

      if( domainContext == null )
        throw new ArgumentNullException( "domainContext" );

      this.EntityQuery = entityQuery;
      this.DomainContext = domainContext;
    }

    #endregion

    #region CanSkip Property

    public override bool CanSkip
    {
      get
      {
        return true;
      }
    }

    #endregion

    #region CanFilter Property

    public override bool CanFilter
    {
      get
      {
        return true;
      }
    }

    #endregion

    #region CanFilterGroup Property

    public sealed override bool CanFilterGroup
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanJump Property

    public sealed override bool CanJump
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanJumpToItem Property

    public sealed override bool CanJumpToItem
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanCount Property

    public override bool CanCount
    {
      get
      {
        return this.EntityQuery.IncludeTotalCount;
      }
    }

    #endregion

    #region CanGroupCount Property

    public sealed override bool CanGroupCount
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanCalculateStats Property

    public override bool CanCalculateStats
    {
      get
      {
        return this.CanCount;
      }
    }

    #endregion

    #region InitialItemCount Property

    public Nullable<long> InitialItemCount
    {
      get;
      set;
    }

    #endregion

    #region ElementType Protected Property

    protected internal sealed override Type ElementType
    {
      get
      {
        return this.EntityQuery.EntityType;
      }
    }

    #endregion

    #region EntityQuery Protected Property

    protected EntityQuery<TEntity> EntityQuery
    {
      get;
      private set;
    }

    #endregion

    #region DomainContext Protected Property

    protected DomainContext DomainContext
    {
      get;
      private set;
    }

    #endregion

    #region IsQueryLengthLimited Internal Property

    internal override bool IsQueryLengthLimited
    {
      get
      {
        return true;
      }
    }

    #endregion

    protected override IEnumerable<SortDescription> GetPrimaryKeyInfo()
    {
      Type entityType = this.ElementType;
      Type attributeType = typeof( KeyAttribute );

      foreach( PropertyInfo propertyInfo in entityType.GetProperties() )
      {
        if( propertyInfo.IsDefined( attributeType, true ) )
        {
          string memberName = string.Intern( propertyInfo.Name );

          yield return new SortDescription( memberName, ListSortDirection.Ascending );
        }
      }

      foreach( FieldInfo fieldInfo in entityType.GetFields() )
      {
        if( fieldInfo.IsDefined( attributeType, true ) )
        {
          string memberName = string.Intern( fieldInfo.Name );

          yield return new SortDescription( memberName, ListSortDirection.Ascending );
        }
      }
    }

    protected override IEnumerable<Type> GetSupportedStatFunctions( Type dataType )
    {
      // Check if we can replace the CountFunction with a call to BeginExecuteCountQuery.
      if( this.CanCount )
      {
        bool mayCount = !dataType.IsNullableType();
        if( ( !mayCount ) && ( this.CanFilter ) )
        {
          // Since the CountFunction may require null values inclusion or exclusion, we need
          // to be able set a filter for null or non-null values.
          List<FilterOperator> filterOperators = this.GetSupportedFilterOperators( dataType ).ToList();

          mayCount = ( filterOperators.Contains( FilterOperator.Equal ) )
                  && ( filterOperators.Contains( FilterOperator.NotEqual ) );
        }

        if( mayCount )
          yield return typeof( CountFunction );
      }

      yield break;
    }

    protected virtual EntityQuery<TEntity> GetEntityQuery( CountQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.FilterGroup == null )
                 && ( queryParameters.Jump == null ) );

      EntityQuery<TEntity> entityQuery = this.EntityQuery;

      if( queryParameters.Filter != null )
      {
        SortRequest sortBy = ( queryParameters.Sort != null )
                               ? queryParameters.Sort
                               : new SortRequest( this.PrimaryKeyInfo );

        entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.OrderBy( entityQuery, sortBy );
        entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Where( entityQuery, queryParameters.Filter );
      }

      entityQuery = ( EntityQuery<TEntity> )entityQuery.Take( 0 );

      return entityQuery;
    }

    protected virtual EntityQuery<TEntity> GetEntityQuery( DataQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.ItemJump == null )
                 && ( queryParameters.Jump == null ) );

      EntityQuery<TEntity> entityQuery = this.EntityQuery;

      entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.OrderBy( entityQuery, queryParameters.Sort );

      if( queryParameters.Filter != null )
      {
        if( queryParameters.Sort == null )
        {
          entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.OrderBy( entityQuery, new SortRequest( this.PrimaryKeyInfo ) );
        }

        entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Where( entityQuery, queryParameters.Filter );
      }

      entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Skip( entityQuery, queryParameters.Skip );
      entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Take( entityQuery, queryParameters.Take );

      return entityQuery;
    }

    protected override IAsyncResult BeginExecuteCountQuery(
      CountQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      EntityQuery<TEntity> entityQuery = this.GetEntityQuery( queryParameters );

      LoadAsyncResult asyncResult = new LoadAsyncResult( callback, state );

      this.DomainContext.Load(
        entityQuery,
        LoadBehavior.MergeIntoCurrent,
        this.OnCountLoadCompleted,
        asyncResult );

      asyncResult.IsSynchronous = false;

      return asyncResult;
    }

    protected override CountQueryResult EndExecuteCountQuery( IAsyncResult asyncResult )
    {
      LoadAsyncResult loadAsyncResult = ( LoadAsyncResult )asyncResult;

      if( loadAsyncResult.Error != null )
        throw loadAsyncResult.Error;

      return ( CountQueryResult )loadAsyncResult.Result;
    }

    protected override IAsyncResult BeginExecuteDataQuery(
      DataQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      EntityQuery<TEntity> entityQuery = this.GetEntityQuery( queryParameters );

      LoadAsyncResult asyncResult = new LoadAsyncResult( callback, state );

      this.DomainContext.Load(
        entityQuery,
        LoadBehavior.MergeIntoCurrent,
        this.OnDataLoadCompleted,
        asyncResult );

      asyncResult.IsSynchronous = false;

      return asyncResult;
    }

    protected override DataQueryResult EndExecuteDataQuery( IAsyncResult asyncResult )
    {
      LoadAsyncResult loadAsyncResult = ( LoadAsyncResult )asyncResult;

      if( loadAsyncResult.Error != null )
        throw loadAsyncResult.Error;

      return ( DataQueryResult )loadAsyncResult.Result;
    }

    protected override IAsyncResult BeginExecuteGroupCountQuery(
      GroupCountQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      throw new NotSupportedException();
    }

    protected override GroupCountQueryResult EndExecuteGroupCountQuery( IAsyncResult asyncResult )
    {
      throw new NotSupportedException();
    }

    protected override IAsyncResult BeginExecuteStatQuery(
      StatQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      CountFunction countFunction = queryParameters.Function.StatFunction as CountFunction;

      if( countFunction == null )
        // The StatFunction isn't supported.
        throw new NotSupportedException();

      // The CountFunction may be handled by a call to BeginExecuteCountQuery.
      FilterRequest filterRequest = queryParameters.Filter;
      FilterExpressionBase filterExpression = ( filterRequest != null ) ? filterRequest.Expression : null;
      string propertyName = countFunction.GetSourcePropertyNames().Single();
      bool isNullableType = this.GetElementMemberType( propertyName ).IsNullableType();

      // Modify the filter expression to take care of the null value.
      if( isNullableType )
      {
        if( countFunction.IncludeNullValues )
        {
          FilterExpression includeNull = new FilterExpression( propertyName, FilterOperator.Equal, null );

          if( filterExpression != null )
          {
            filterExpression = new OrFilterExpression( includeNull, filterExpression );
          }
          else
          {
            filterExpression = includeNull;
          }
        }
        else
        {
          FilterExpression excludeNull = new FilterExpression( propertyName, FilterOperator.NotEqual, null );

          if( filterExpression != null )
          {
            filterExpression = new AndFilterExpression( excludeNull, filterExpression );
          }
          else
          {
            filterExpression = excludeNull;
          }
        }
      }

      // Create the new FilterRequest for the count query.
      if( filterRequest != null )
      {
        filterRequest = new FilterRequest( filterRequest.Filter, filterExpression );
      }
      else if( filterExpression != null )
      {
        filterRequest = new FilterRequest( filterExpression );
      }
      else
      {
        filterRequest = null;
      }

      // Start the count query.
      CountQueryParameters newQueryParameters = new CountQueryParameters(
                                                  queryParameters.Sort,
                                                  filterRequest,
                                                  null,
                                                  queryParameters.FilterGroup );

      return this.BeginExecuteCountQuery( newQueryParameters, callback, state );
    }

    protected override StatQueryResult EndExecuteStatQuery( IAsyncResult asyncResult )
    {
      CountQueryResult queryResult = this.EndExecuteCountQuery( asyncResult );

      return new StatQueryResult( new StatResult( queryResult.Count ) );
    }

    private static EntityQuery<TEntity> Take(
      EntityQuery<TEntity> entityQuery,
      TakeRequest request )
    {
      if( request == null )
        return entityQuery;

      return entityQuery.Take<TEntity>( request.Count );
    }

    private static EntityQuery<TEntity> Skip(
      EntityQuery<TEntity> entityQuery,
      SkipRequest request )
    {
      if( ( request == null ) || ( request.Count == 0 ) )
        return entityQuery;

      long skip = request.Count;

      while( skip > 0 )
      {
        if( skip > int.MaxValue )
        {
          entityQuery = entityQuery.Skip<TEntity>( int.MaxValue );
          skip -= int.MaxValue;
        }
        else
        {
          entityQuery = entityQuery.Skip<TEntity>( Convert.ToInt32( skip ) );
          skip = 0;
        }
      }

      return entityQuery;
    }

    private static EntityQuery<TEntity> Where(
      EntityQuery<TEntity> entityQuery,
      FilterRequest request )
    {
      if( request == null )
        return entityQuery;

      Debug.Assert( ( request.Filter == null ) && ( request.Expression != null ) );

      ExpressionBuilder eb = new ExpressionBuilder( request.Expression );
      Expression<Func<TEntity, bool>> predicate = eb.ToExpression<TEntity>();

      if( predicate == null )
        return entityQuery;

      return entityQuery.Where<TEntity>( predicate );
    }

    private static EntityQuery<TEntity> OrderBy(
      EntityQuery<TEntity> entityQuery,
      SortRequest request )
    {
      if( ( request == null ) || ( request.SortDescriptions.Count <= 0 ) )
        return entityQuery;

      bool isFirstSort = true;
      Type entityType = entityQuery.EntityType;

      foreach( SortDescription sort in request.SortDescriptions )
      {
        LambdaExpression keySelector = WcfRiaServicesDataSourceProviderBase<TEntity>.CreateKeySelectorExpression( sort.PropertyName, entityType );
        Type[] keySelectorGenericType = null;

        if( keySelector.Type.IsGenericType )
        {
          keySelectorGenericType = keySelector.Type.GetGenericArguments();
        }

        Debug.Assert( ( keySelectorGenericType != null ) && ( keySelectorGenericType.Length == 2 ) );

        MethodInfo sortingMethod;

        if( sort.Direction == ListSortDirection.Ascending )
        {
          sortingMethod = ( isFirstSort )
                            ? WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "OrderBy", keySelectorGenericType )
                            : WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "ThenBy", keySelectorGenericType );
        }
        else
        {
          sortingMethod = ( isFirstSort )
                            ? WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "OrderByDescending", keySelectorGenericType )
                            : WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "ThenByDescending", keySelectorGenericType );
        }

        isFirstSort = false;

        entityQuery = ( EntityQuery<TEntity> )sortingMethod.Invoke(
                                                entityQuery,
                                                new object[] { 
                                                  entityQuery, 
                                                  keySelector } );
      }

      return entityQuery;
    }

    private static LambdaExpression CreateKeySelectorExpression(
      string propertyName,
      Type entityType )
    {
      bool isNullableExpression;
      ParameterExpression parameter = Expression.Parameter( entityType );
      Expression member = ComplexStringHelper.GetExpressionFromString( propertyName, entityType, parameter, false, out isNullableExpression );

      return Expression.Lambda( member, parameter );
    }

    private static MethodInfo FindEntityQueryableMethod(
      string methodName,
      params Type[] genericTypes )
    {
      MethodInfo methodInfo = typeof( EntityQueryable ).GetMethod( methodName );

      if( methodInfo != null )
      {
        if( methodInfo.IsGenericMethod )
        {
          methodInfo = methodInfo.MakeGenericMethod( genericTypes );
        }
        else
        {
          methodInfo = null;
        }
      }

      if( methodInfo == null )
        throw new ArgumentException( "The specified method was not found on the EntityQueryable class." );

      return methodInfo;
    }

    private bool TryGetInlineCount( LoadOperation<TEntity> operation, out long inlineCount )
    {
      inlineCount = 0;

      bool found = false;
      long count = operation.TotalEntityCount;

      // A LoadOperation may give a negative entity count value to identify a special state.
      if( count >= 0 )
      {
        inlineCount = count;
        found = true;
      }

      return found;
    }

    private void OnCountLoadCompleted( LoadOperation<TEntity> operation )
    {
      LoadAsyncResult asyncResult = ( LoadAsyncResult )operation.UserState;
      long totalCount = 0;

      if( operation.HasError )
      {
        asyncResult.Error = operation.Error;
      }
      else if( !this.TryGetInlineCount( operation, out totalCount ) )
      {
        try
        {
          // The exception must be thrown on the call to EndExecuteCountQuery.
          // We throw the exception within a try/catch block to get a call stack.
          throw new InvalidOperationException( "The total item count was not provided by the server." );
        }
        catch( Exception e )
        {
          asyncResult.Error = e;
        }
      }
      else
      {
        asyncResult.Result = new CountQueryResult( totalCount );
      }

      asyncResult.Complete();
    }

    private void OnDataLoadCompleted( LoadOperation<TEntity> operation )
    {
      LoadAsyncResult asyncResult = ( LoadAsyncResult )operation.UserState;

      if( operation.HasError )
      {
        asyncResult.Error = operation.Error;
      }
      else
      {
        Nullable<long> inlineCount = null;
        long count = 0;

        if( this.TryGetInlineCount( operation, out count ) )
        {
          inlineCount = count;
        }

        asyncResult.Result = new DataQueryResult( operation.Entities, inlineCount );
      }

      asyncResult.Complete();
    }

    #region LoadAsyncResult Nested Type

    private class LoadAsyncResult : IAsyncResult
    {
      internal LoadAsyncResult( AsyncCallback callback, object state )
      {
        m_callback = callback;
        m_state = state;
      }

      internal bool IsSynchronous
      {
        get
        {
          return m_isSynchronous;
        }
        set
        {
          if( !m_isCompleted )
          {
            m_isSynchronous = value;
          }
        }
      }

      internal object Result
      {
        get
        {
          return m_result;
        }
        set
        {
          if( !m_isCompleted )
          {
            m_result = value;
          }
        }
      }

      internal Exception Error
      {
        get
        {
          return m_error;
        }
        set
        {
          if( !m_isCompleted )
          {
            m_error = value;
          }
        }
      }

      object IAsyncResult.AsyncState
      {
        get
        {
          return m_state;
        }
      }

      WaitHandle IAsyncResult.AsyncWaitHandle
      {
        get
        {
          lock( m_lock )
          {
            if( m_waitHandle == null )
            {
              m_waitHandle = new ManualResetEvent( m_isCompleted );
            }
          }

          return m_waitHandle;
        }
      }

      bool IAsyncResult.CompletedSynchronously
      {
        get
        {
          return m_isCompleted && m_isSynchronous;
        }
      }

      bool IAsyncResult.IsCompleted
      {
        get
        {
          return m_isCompleted;
        }
      }

      internal void Complete()
      {
        lock( m_lock )
        {
          if( m_isCompleted )
            return;

          m_isCompleted = true;

          if( m_waitHandle != null )
          {
            m_waitHandle.Set();
          }
        }

        if( m_callback != null )
        {
          m_callback.Invoke( this );
        }
      }

      private AsyncCallback m_callback; //null
      private object m_state; //null
      private ManualResetEvent m_waitHandle; //null
      private bool m_isCompleted; //false
      private bool m_isSynchronous = true;
      private object m_result; //null
      private Exception m_error; //null
      private object m_lock = new object();
    }

    #endregion

    #region ExpressionBuilder Nested Type

    private class ExpressionBuilder : PredicateExpressionBuilder
    {
      internal ExpressionBuilder( FilterExpressionBase expression )
        : base( expression )
      {
      }

      protected override Expression<Func<T, bool>> CreateTautology<T>()
      {
        return null;
      }
    }

    #endregion
  }
}
